<?php
/**
 * 
 * @copyright Copyright (c) 2020 cleverstone
 * 
 */

namespace app\builder\common;

use Yii;
use yii\db\Query;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
use app\behaviors\DatetimeBehavior;
use yii\db\StaleObjectException;

/**
 * 模型继承类
 * @property-read string $error 错误信息
 * @author cleverstone
 * @since ym1.0
 */
class CommonActiveRecord extends ActiveRecord
{
    /**
     * 附加公共行为
     * @return array
     */
    public function behaviors()
    {
        return [
            // 日期处理器
            'timestampBehavior' => [
                'class' => DatetimeBehavior::class,
                'attributes' => [
                    CommonActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    CommonActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
                ],
            ],
        ];
    }

    /**
     * Saves the changes to this active record into the associated database table.
     *
     * This method performs the following steps in order:
     *
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
     *    returns `false`, the rest of the steps will be skipped;
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
     *    failed, the rest of the steps will be skipped;
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
     *    the rest of the steps will be skipped;
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
     * 5. call [[afterSave()]];
     *
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
     * will be raised by the corresponding methods.
     *
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
     *
     * For example, to update a customer record:
     *
     * ```php
     * $customer = Customer::findOne($id);
     * $customer->name = $name;
     * $customer->email = $email;
     * $customer->updateByAndWhere(true, ['status' => 1]);
     * ```
     *
     * Note that it is possible the update does not affect any row in the table.
     * In this case, this method will return 0. For this reason, you should use the following
     * code to check if update() is successful or not:
     *
     * ```php
     * if ($customer->updateByAndWhere(true, ['status' => 1]) !== false) {
     *     // update successful
     * } else {
     *     // update failed
     * }
     * ```
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
     * being updated is outdated.
     * @throws \Throwable in case update failed.
     */
    public function updateByAndWhere($runValidation = true, $addCondition = [], $params = [], $attributeNames = null)
    {
        return $this->updateBy($runValidation, $addCondition, $params, 'and', $attributeNames);
    }

    /**
     * @see updateByAndWhere
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
     * being updated is outdated.
     * @throws \Throwable in case update failed.
     */
    public function updateByOrWhere($runValidation = true, $addCondition = [], $params = [], $attributeNames = null)
    {
        return $this->updateBy($runValidation, $addCondition, $params, 'or', $attributeNames);
    }

    /**
     * @see updateByOrWhere
     * @see updateByAndWhere
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array $operator operator for conditions.
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
     * being updated is outdated.
     * @throws \Throwable in case update failed.
     */
    protected function updateBy($runValidation = true, $addCondition = [], $params = [], $operator = 'and', $attributeNames = null)
    {
        if ($runValidation && !$this->validate($attributeNames)) {
            Yii::info('Model not updated due to validation error.', __METHOD__);
            return false;
        }

        if (!$this->isTransactional(self::OP_UPDATE)) {
            return $this->updateByInternal($addCondition, $params, $operator, $attributeNames);
        }

        $transaction = static::getDb()->beginTransaction();
        try {
            $result = $this->updateByInternal($addCondition, $params, $operator, $attributeNames);
            if ($result === false) {
                $transaction->rollBack();
            } else {
                $transaction->commit();
            }

            return $result;
        } catch (\Exception $e) {
            $transaction->rollBack();
            throw $e;
        } catch (\Throwable $e) {
            $transaction->rollBack();
            throw $e;
        }
    }

    /**
     * @see updateBy()
     *
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array $operator operator for conditions.
     * @param array|null $attributes attributes to update
     * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException
     */
    protected function updateByInternal($addCondition = [], $params = [], $operator = 'and', $attributes = null)
    {
        if (!$this->beforeSave(false)) {
            return false;
        }
        $values = $this->getDirtyAttributes($attributes);
        if (empty($values)) {
            $this->afterSave(false, $values);
            return 0;
        }
        $condition = $this->getOldPrimaryKey(true);
        $lock = $this->optimisticLock();
        if ($lock !== null) {
            $values[$lock] = $this->$lock + 1;
            $condition[$lock] = $this->$lock;
        }
        // We do not check the return value of updateAll() because it's possible
        // that the UPDATE statement doesn't change anything and thus returns 0.
        $rows = static::updateAll($values, [$operator, $condition, $addCondition], $params);

        if ($lock !== null && !$rows) {
            throw new StaleObjectException('The object being updated is outdated.');
        }

        if (isset($values[$lock])) {
            $this->$lock = $values[$lock];
        }

        $changedAttributes = [];
        foreach ($values as $name => $value) {
            $changedAttributes[$name] = $this->getOldAttribute($name);
            $this->setOldAttribute($name, $value);
        }
        $this->afterSave(false, $changedAttributes);

        return $rows;
    }

    /**
     * 获取当前模型映射的ActiveQuery对象[调用关联查询，必须声明关联关系，否则附表结果通不过安全检查，字段结果无法显示]
     * 建议：简单查询使用该方法
     * @param array|string $select 查询字段
     * @return ActiveQuery
     */
    public static function activeQuery($select = '*')
    {
        return self::find()->select($select);
    }

    /**
     * 获取Query查询对象[调用关联查询，不用声明关联关系]
     * 建议：关联查询使用该方法
     * @param array|string $select 查询字段
     * @param string 主表别名
     * @return Query
     * @throws
     */
    public static function query($select = '*', $alias = '')
    {
        /* @var Query $query */
        $query = Yii::createObject(Query::className());

        if (empty($alias)) {
            return $query->from(static::tableName())->select($select);
        }

        return $query->from([$alias => static::tableName()])->select($select);
    }

    /**
     * 获取验证错误信息[字符串格式]
     * @return string
     */
    public function getError()
    {
        $firstErrors = $this->firstErrors;
        return reset($firstErrors) ?: t('unknown error', 'app.admin');
    }

    /**
     * {@inheritdoc}
     */
    public function load($data, $formName = '')
    {
        return parent::load($data, $formName);
    }
}